Skip to content

MOBILE-342: Embedded blocks — the public API for React Native - #218

Closed
Vailence wants to merge 19 commits into
mission/storiesfrom
feature/MOBILE-342
Closed

MOBILE-342: Embedded blocks — the public API for React Native#218
Vailence wants to merge 19 commits into
mission/storiesfrom
feature/MOBILE-342

Conversation

@Vailence

Copy link
Copy Markdown
Collaborator

Adds MindboxEmbeddedBlock — the React Native wrapper over the SDK's own embedded block, the same
container SwiftUI, Compose and Flutter wrap. The app marks a place by its system name and hands it
a height; what goes into that place is the config's decision and changes without an app release.

Public API

import { MindboxEmbeddedBlock } from 'mindbox-sdk'

const active = useIsFocused()   // one per screen

<MindboxEmbeddedBlock
  placeSystemName="stories"
  height={104}
  active={active}
  timeoutMs={5000}
  placeholder={<StoriesSkeleton />}
  error={<StoriesUnavailable />}
  onLoad={() => analytics.track('stories_shown')}
  onFail={() => setShowStoriesSection(false)}
/>

Seven ideas, the same ones the other wrappers expose: the place, the host's height, the waiting
budget, two screens of the host's own, two outcomes — plus one thing only React Native needs.

Prop Behaviour
placeSystemName Taken exactly as given, nothing trimmed. A different name is a different block, built from scratch.
height Live — a new value resizes the block in place, no reload. Non-finite or negative reserves no space and warns.
timeoutMs The budget for the answer, default 30 s. Fixed at creation, as in every other wrapper: a change on a live block is ignored with a warning.
placeholder / error Ordinary RN nodes drawn above the native view, so they resolve the host's theme and context. error is the opt-in into showing a failure at all; an empty place always collapses.
onLoad / onFail One delivery per outcome. onFail covers a failure, a timeout and an empty place alike.
active RN-only. Every RN screen lives in one native window, so a covered screen never leaves it and the container cannot see that nobody is looking. Without it a block spends its whole budget behind another screen; with it, leaving is a pause — the page and the remaining budget are kept for the return.

Implementation

A Fabric component; the module is New Architecture only, so no old-architecture path exists.

  • AndroidSimpleViewManager over a frame that holds the SDK's MindboxEmbeddedBlockView.
    The frame installs its own LifecycleOwner: the block must live as long as the RN view, not as
    long as the fragment react-native-screens destroys under a covering screen. It also re-measures
    itself on the looper, because Yoga answers a native child's requestLayout() with nothing.
  • iOSRCTViewComponentView (Objective-C++, as the base class and event emitters are C++)
    driving a Swift host, since the container's wrapper API is behind @_spi(Internal). The block is
    built in finalizeUpdates:, and appearances reported before the emitter exists are replayed once
    it arrives.
  • Recycling is off on both sides: a released block cannot be revived, so it is created and destroyed
    with its view.
  • The host's placeholder and error reach the container as empty stand-in views — it is told the place
    is taken, not what to draw there. The visible screens are RN overlays.

Notes for review

  • Native SDK versions stay at the current release pins (2.15.2 / 2.15.1). Embedded blocks are not
    released yet, so this branch is built against local builds through the existing
    mindboxNativeSdkVersion override and a :path pod — those overrides are deliberately not
    committed. The wire contract itself is already stable on mission/stories in all three native SDKs.
  • CHANGELOG is left untouched until the release.

Checks

  • 12 new component tests (37 in the suite), tsc clean over the feature.
  • Android codegen + Kotlin compile, iOS MindboxSdk scheme builds.
  • Hand-checked on an iPhone 16 Pro simulator and an API 36 emulator against a staging stand: content,
    empty place, custom timeout, host placeholder and error, taps and horizontal swipes reaching the
    native content through the overlay, and push/modal cover-and-return keeping the content in place.

@Vailence

Vailence commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Спасибо за разбор — все четыре пункта закрыты в 1112280. Ниже по каждому, плюс две вещи, которые всплыли по дороге.

Обязательный: lint

Зелёный. Одна поправка к рецепту: голый yarn lint --fix чинит не только prettier — он «исправляет» ещё и @react-native/no-deep-imports, переписывая

import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'

на импорт из корня react-native. Кодогенерации это безразлично (парсер смотрит на имя вызова), но корневого ре-экспорта нет в RN < 0.82, а в peerDependencies у нас >= 0.76 — то есть автофикс молча ломает совместимость с минимальной поддерживаемой версией. Чинил точечно:

npx eslint "**/*.{js,ts,tsx}" --fix --fix-type layout

Плюс руками схлопнул конкатенации в шаблонные литералы: printWidth здесь 1000, поэтому prettier строки не переносит, а склеивает — и + '…' посреди строки превращается в шум.

Осталось 0 ошибок и 2 предупреждения, оба преэкзистирующие: no-inline-styles на вычисляемой высоте и тот самый no-deep-imports. yarn lint в CI без --max-warnings, так что не валят.

Средний: пустое имя места

Исправил ровно как предложено — гард больше не отказывается строить хост, и это подтвердилось на устройстве: блок схлопывается в 0 и отдаёт onFail, лог из Swift-хоста теперь достижим. JS-предупреждение расширил на бланк, но не двумя if, а if / else if: иначе имя из одних пробелов ловило бы сразу два предупреждения. Имя в текст бланкового варианта не подставляю — The block "" читается как опечатка в самом сообщении.

Но заодно выяснилось, что эталоном тут был не Android. На эмуляторе безымянное место даёт onFail, а collapsed в appearance-обсервер не приходит — место остаётся занятым на всю высоту. В логах видно, почему:

E/Mindbox: MindboxEmbeddedBlockView: [EmbeddedBlock] app:mindboxPlaceSystemName is not set on the block:
           it has nothing to resolve and stays hidden.

Нативный Android прячет свою вьюху (visibility = GONE) вместо того, чтобы сообщить обёртке COLLAPSED, — а обёртка про visibility ничего не знает и продолжает держать высоту. То есть после этого PR платформы всё ещё расходятся, только теперь наоборот: правильно ведёт себя iOS. Чинить надо в нативном Android SDK, обёртке додумывать за контейнер нечем. Завести на MOBILE-341/419?

Низкий: нулевая высота

Выровнял iOS по Android — в гард добавился CGRectIsEmpty(self.bounds). Отдельный updateLayoutMetrics: не понадобился: в RCTMountingManager.mm (случай ShadowViewMutation::Update) изменение одних только layout-метрик взводит RNComponentViewUpdateMaskLayoutMetrics, а вызов finalizeUpdates: стоит под if (mask != RNComponentViewUpdateMaskNone) — значит блок построится в первый же ненулевой фрейм. Порядок «метрики до finalizeUpdates:» там же зашит в код, и в 0.82, и в 0.74 файл идентичен.

Побочно чинится настоящий баг, который был виден на демо: блок в раскрывающемся контейнере (высота 0 → N) верстал страницу в нулевом вьюпорте и рапортовал onLoad о невидимой ленте. Теперь при раскрытии лента верстается на полную ширину — проверил.

Контракт height <= 0 («блок не строится и об исходе не сообщает») теперь одинаков на обеих платформах; записал в JSDoc пропа и строкой в README.

Низкий: хвосты pending-сигналов

dropHost дочищает оба поля.

И там же нашлась причина посерьёзнее. prepareForRecycle — мёртвый код: RN зовёт его только для рециклируемых вью, а у нас +shouldBeRecycled = NO, значит вызывается invalidate (RCTComponentViewRegistry.mm:108), который мы не переопределяли. Из-за этого release() блока на iOS происходил только в dealloc — недетерминированно, WKWebView жил до слива autorelease pool, тогда как Android честно рвёт блок в onDropViewInstance. Заменил метод на invalidate; tearDown идемпотентен, так что последующий dealloc безвреден.

Проверки

40 jest-тестов (добавил 3: пустое имя, имя из пробелов, доставка исхода безымянного места), tsc чистый, обе нативные сборки собираются. Руками на iPhone 16 Pro и API 36: регрессии контента нет, пустое имя схлопывается с onFail, раскрывающийся контейнер верстает страницу правильно, смена места не мигает чужим исходом.

Про условия мержа и решение по trim — согласен, ждём релиза нативных SDK и командного решения; пины и CHANGELOG трогать не стал.

@Vailence
Vailence changed the base branch from new-arch to mission/stories September 2, 2026 11:15
@Vailence Vailence closed this Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants